package dev.turingcomplete.intellijbytecodeplugin.view._internal._structure._class

import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.vfs.JarFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.jrt.JrtFileSystem
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem
import com.intellij.util.text.DateFormatUtil
import dev.turingcomplete.intellijbytecodeplugin.ByteCodeAnalyzerBundle
import dev.turingcomplete.intellijbytecodeplugin.bytecode.AccessGroup
import dev.turingcomplete.intellijbytecodeplugin.bytecode.ClassVersionUtils
import dev.turingcomplete.intellijbytecodeplugin.bytecode.MethodDeclarationUtils
import dev.turingcomplete.intellijbytecodeplugin.bytecode.TypeUtils
import dev.turingcomplete.intellijbytecodeplugin.org.objectweb.asm.Opcodes
import dev.turingcomplete.intellijbytecodeplugin.org.objectweb.asm.tree.ClassNode
import dev.turingcomplete.intellijbytecodeplugin.view._internal._structure.GoToProvider
import dev.turingcomplete.intellijbytecodeplugin.view._internal._structure._common.HtmlTextNode
import dev.turingcomplete.intellijbytecodeplugin.view._internal._structure._common.HyperLinkNode
import dev.turingcomplete.intellijbytecodeplugin.view._internal._structure._common.TextNode
import dev.turingcomplete.intellijbytecodeplugin.view._internal._structure._common.ValueNode
import org.apache.commons.io.FileUtils
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.BasicFileAttributes
import javax.swing.Icon

internal class ClassStructureNode(private val classNode: ClassNode, private val classFile: VirtualFile)
  : ValueNode(displayValue = { cxt -> TypeUtils.toReadableName(classNode.name, cxt.typeNameRenderMode) },
              icon = determineClassIcon(classNode),
              goToProvider = GoToProvider.Class(classNode.name)) {

  // -- Companion Object -------------------------------------------------------------------------------------------- //

  companion object {
    private val LOG = Logger.getInstance(ClassStructureNode::class.java)

    private fun determineClassIcon(classNode: ClassNode): Icon {
      return when {
        classNode.access and Opcodes.ACC_INTERFACE != 0 -> AllIcons.Nodes.Interface
        classNode.access and Opcodes.ACC_ENUM != 0 -> AllIcons.Nodes.Enum
        classNode.access and Opcodes.ACC_ABSTRACT != 0 -> AllIcons.Nodes.AbstractClass
        classNode.access and Opcodes.ACC_RECORD != 0 -> AllIcons.Nodes.Record
        else -> AllIcons.Nodes.Class
      }
    }
  }

  // -- Properties -------------------------------------------------------------------------------------------------- //
  // -- Initialization ---------------------------------------------------------------------------------------------- //

  init {
    asyncAdd(true) {
      addClassVersionNode()
      addAccessNode(classNode.access, if (classNode.module != null) AccessGroup.MODULE else AccessGroup.CLASS)
      addSignatureNode(classNode.signature)
      addSuperNameNode()
      addInterfacesNode()
      addDebugNode()
      addModuleNode()
      addOuterClassNode()
      addOuterMethodNode()
      addAttributesNode(classNode.attrs)
      addInnerClasses()
      addNestNode()
      addAnnotationsNode(ByteCodeAnalyzerBundle.message("annotations"), classNode.visibleAnnotations, classNode.invisibleAnnotations)
      addAnnotationsNode(ByteCodeAnalyzerBundle.message("type.annotations"), classNode.visibleTypeAnnotations, classNode.invisibleTypeAnnotations)
      addFieldsNode()
      addMethodsNode()
      addRecordComponentsNode()
      addPermittedSubclassesNode()
      addFileNode()
    }
  }

  // -- Exposed Methods --------------------------------------------------------------------------------------------- //
  // -- Private Methods --------------------------------------------------------------------------------------------- //

  private fun addClassVersionNode() {
    add(HtmlTextNode(ByteCodeAnalyzerBundle.message("class.version"), classNode.version.toString(),
                     postFix = ClassVersionUtils.toClassVersion(classNode.version)?.let {
                       "<span class=\"contextHelp\">${it.specification}</span>"
                     },
                     icon = AllIcons.FileTypes.Java))
  }

  private fun addSuperNameNode() {
    classNode.superName?.let { superName ->
      add(ValueNode(ByteCodeAnalyzerBundle.message("super"),
                    { ctx -> TypeUtils.toReadableName(superName, ctx.typeNameRenderMode) },
                    icon = AllIcons.Hierarchy.Supertypes,
                    goToProvider = GoToProvider.Class(superName)))
    }
  }

  private fun addDebugNode() {
    if (classNode.sourceFile == null && classNode.sourceDebug == null) {
      return
    }

    add(TextNode(ByteCodeAnalyzerBundle.message("debug.information"), AllIcons.Actions.StartDebugger).apply {
      classNode.sourceFile?.let { sourceFile ->
        add(ValueNode(ByteCodeAnalyzerBundle.message("source.file"), sourceFile, icon = AllIcons.FileTypes.Java))
      }

      classNode.sourceDebug?.let { sourceDebug ->
        add(ValueNode(ByteCodeAnalyzerBundle.message("source.debug"), sourceDebug))
      }
    })
  }

  private fun addModuleNode() {
    classNode.module?.let {
      add(ModuleStructureNode(it))
    }
  }

  private fun addOuterClassNode() {
    classNode.outerClass?.let { outerClass ->
      add(ValueNode(ByteCodeAnalyzerBundle.message("outer.class"), { ctx -> TypeUtils.toReadableName(outerClass, ctx.typeNameRenderMode) },
                    goToProvider = GoToProvider.Class(outerClass)))
    }
  }

  private fun addOuterMethodNode() {
    classNode.outerMethod?.let { outerMethod ->
      add(ValueNode(ByteCodeAnalyzerBundle.message("outer.method"),
                    { ctx -> MethodDeclarationUtils.toReadableDeclaration(outerMethod, classNode.outerMethodDesc ?: "", classNode.outerMethodDesc ?: "", ctx.typeNameRenderMode, ctx.methodDescriptorRenderMode, true) },
                    { ctx -> MethodDeclarationUtils.toReadableDeclaration(outerMethod, classNode.outerMethodDesc ?: "", classNode.outerMethodDesc ?: "", ctx.typeNameRenderMode, ctx.methodDescriptorRenderMode, false) }
                   ))
    }
  }

  private fun addNestNode() {
    if (classNode.nestHostClass == null && classNode.nestMembers?.isNotEmpty() != true) {
      return
    }

    add(TextNode(ByteCodeAnalyzerBundle.message("nest"), AllIcons.Actions.GroupBy).apply {
      classNode.nestHostClass?.let { nestHostClass ->
        add(ValueNode(ByteCodeAnalyzerBundle.message("nest.host"),
                      { ctx -> TypeUtils.toReadableName(nestHostClass, ctx.typeNameRenderMode) },
                      goToProvider = GoToProvider.Class(nestHostClass)))
      }

      addTitleNodeWithElements(classNode.nestMembers, { TextNode(ByteCodeAnalyzerBundle.message("nest.members")) }) { _, nestMember ->
        ValueNode(displayValue = { ctx -> TypeUtils.toReadableName(nestMember, ctx.typeNameRenderMode) },
                  goToProvider = GoToProvider.Class(nestMember))
      }
    })
  }

  private fun addInnerClasses() {
    addTitleNodeWithElements(classNode.innerClasses, { TextNode(ByteCodeAnalyzerBundle.message("inner.classes"), AllIcons.Nodes.Class) }) { _, innerClass ->
      ValueNode(displayValue = { ctx -> TypeUtils.toReadableName(innerClass.name, ctx.typeNameRenderMode) },
                icon = AllIcons.Nodes.Class,
                goToProvider = GoToProvider.Class(innerClass.name))
    }
  }

  private fun addMethodsNode() {
    addTitleNodeWithElements(classNode.methods, { TextNode(ByteCodeAnalyzerBundle.message("methods"), AllIcons.Nodes.Method) }) { _, method ->
      MethodStructureNode(method, classNode)
    }
  }

  private fun addInterfacesNode() {
    addTitleNodeWithElements(classNode.interfaces, { TextNode(ByteCodeAnalyzerBundle.message("interfaces"), AllIcons.Nodes.Interface) }) { _, `interface` ->
      ValueNode(displayValue = { ctx -> TypeUtils.toReadableName(`interface`, ctx.typeNameRenderMode) },
                icon = AllIcons.Nodes.Interface,
                goToProvider = GoToProvider.Class(`interface`))
    }
  }

  private fun addFieldsNode() {
    addTitleNodeWithElements(classNode.fields, { TextNode(ByteCodeAnalyzerBundle.message("fields"), AllIcons.Nodes.Field) }) { _, field ->
      FieldStructureNode(field)
    }
  }

  private fun addRecordComponentsNode() {
    addTitleNodeWithElements(classNode.recordComponents, { TextNode(ByteCodeAnalyzerBundle.message("record.components"), AllIcons.Nodes.Record) }) { _, recordComponent ->
      TextNode(recordComponent.name, icon = AllIcons.Nodes.Record).apply {
        asyncAdd {
          recordComponent.descriptor?.let { ValueNode(ByteCodeAnalyzerBundle.message("descriptor"), it) }
          addSignatureNode(recordComponent.signature)
          addAnnotationsNode(ByteCodeAnalyzerBundle.message("annotations"), recordComponent.visibleAnnotations, recordComponent.invisibleAnnotations)
          addAnnotationsNode(ByteCodeAnalyzerBundle.message("type.annotations"), recordComponent.visibleTypeAnnotations, recordComponent.invisibleTypeAnnotations)
          addAttributesNode(recordComponent.attrs)
        }
      }
    }
  }

  private fun addPermittedSubclassesNode() {
    addTitleNodeWithElements(classNode.permittedSubclasses, { TextNode(ByteCodeAnalyzerBundle.message("permitted.subclasses"), AllIcons.General.OverridingMethod) }) { _, subclass ->
      ValueNode(displayValue = { ctx -> TypeUtils.toReadableName(subclass, ctx.typeNameRenderMode) },
                icon = AllIcons.General.OverridingMethod,
                goToProvider = GoToProvider.Class(subclass))
    }
  }

  private fun addFileNode() {
    if (!classFile.isValid || !classFile.exists()) {
      return
    }

    val isEntryInArchive = classFile.fileSystem is ArchiveFileSystem
    val nioPath = classFile.fileSystem.getNioPath(classFile)
    if (!isEntryInArchive && nioPath == null) {
      return
    }

    add(TextNode(ByteCodeAnalyzerBundle.message("file"), AllIcons.FileTypes.Any_type).apply {
      asyncAdd(true) {
        var parentDirectory = classFile.parent

        if (isEntryInArchive) {
          val archive = (classFile.fileSystem as ArchiveFileSystem).getLocalByEntry(classFile)
          parentDirectory = archive?.parent
          val archiveTypeName = when (classFile.fileSystem) {
            is JarFileSystem -> "JAR"
            is JrtFileSystem -> ByteCodeAnalyzerBundle.message("java.runtime.image")
            else -> ByteCodeAnalyzerBundle.message("archive")
          }
          add(ValueNode("Entry of $archiveTypeName:", archive?.name ?:
          ByteCodeAnalyzerBundle.message("unknown")))
        }

        addNioPathNodes(nioPath)

        if (parentDirectory != null && parentDirectory.isInLocalFileSystem) {
          add(HyperLinkNode(ByteCodeAnalyzerBundle.message("open.enclosing.directory")) {
            _, _ -> BrowserUtil.browse(parentDirectory) })
        }
      }
    })
  }

  private fun TextNode.addNioPathNodes(nioPath: Path?) {
    nioPath ?: return

    try {
      add(ValueNode(ByteCodeAnalyzerBundle.message("owner"), Files.getOwner(nioPath).name))

      val attributes = Files.readAttributes(nioPath, BasicFileAttributes::class.java)
      add(ValueNode(ByteCodeAnalyzerBundle.message("size"), FileUtils.byteCountToDisplaySize(attributes.size())))
      add(ValueNode(ByteCodeAnalyzerBundle.message("creation.time"), DateFormatUtil.formatDateTime(attributes.creationTime().toMillis())))
      add(ValueNode(ByteCodeAnalyzerBundle.message("last.modified.time"), DateFormatUtil.formatDateTime(attributes.lastModifiedTime().toMillis())))
      add(ValueNode(ByteCodeAnalyzerBundle.message("last.access.time"), DateFormatUtil.formatDateTime(attributes.lastAccessTime().toMillis())))

      val writable = Files.isWritable(nioPath)
      val writableValue = if (writable) ByteCodeAnalyzerBundle.message("file.is.writeable") else ByteCodeAnalyzerBundle.message("file.is.readonly")
      val writableIcon = if (writable) AllIcons.Ide.Readwrite else AllIcons.Ide.Readonly
      add(ValueNode(displayValue = writableValue, icon = writableIcon))
    }
    catch (e: IOException) {
      LOG.warn("Failed to read parameters of file: $nioPath", e)
      add(TextNode(ByteCodeAnalyzerBundle.message("failed.to.read.file.parameters.0", e.message)))
    }
  }

  // -- Inner Type -------------------------------------------------------------------------------------------------- //
}